<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK IT ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006-2014 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: 麦当苗儿 <zuojiazi@vip.qq.com> <http://www.zjzit.cn>
// +----------------------------------------------------------------------
namespace Think;
class Upload {
	/**
	 * 默认上传配置
	 * @var array
	 */
	private $config = array('mimes' => array(), //允许上传的文件MiMe类型
	'maxSize' => 0, //上传的文件大小限制 (0-不做限制)
	'exts' => array(), //允许上传的文件后缀
	'autoSub' => true, //自动子目录保存文件
	'subName' => array('date', 'Y-m-d'), //子目录创建方式，[0]-函数名，[1]-参数，多个参数使用数组
	'rootPath' => './Uploads/', //保存根路径
	'savePath' => '', //保存路径
	'saveName' => array('uniqid', ''), //上传文件命名规则，[0]-函数名，[1]-参数，多个参数使用数组
	'saveExt' => '', //文件保存后缀，空则使用原后缀
	'replace' => false, //存在同名是否覆盖
	'hash' => true, //是否生成hash编码
	'callback' => false, //检测文件是否存在回调，如果存在返回文件信息数组
	'driver' => '', // 文件上传驱动
	'driverConfig' => array(), // 上传驱动配置
	);

	/**
	 * 上传错误信息
	 * @var string
	 */
	private $error = '';
	//上传错误信息

	/**
	 * 上传驱动实例
	 * @var Object
	 */
	private $uploader;

	/**
	 * 构造方法，用于构造上传实例
	 * @param array  $config 配置
	 * @param string $driver 要使用的上传驱动 LOCAL-本地上传驱动，FTP-FTP上传驱动
	 */
	public function __construct($config = array(), $driver = '', $driverConfig = null) {
		/* 获取配置 */
		$this -> config = array_merge($this -> config, $config);

		/* 设置上传驱动 */
		$this -> setDriver($driver, $driverConfig);

		/* 调整配置，把字符串配置参数转换为数组 */
		if (!empty($this -> config['mimes'])) {
			if (is_string($this -> mimes)) {
				$this -> config['mimes'] = explode(',', $this -> mimes);
			}
			$this -> config['mimes'] = array_map('strtolower', $this -> mimes);
		}
		if (!empty($this -> config['exts'])) {
			if (is_string($this -> exts)) {
				$this -> config['exts'] = explode(',', $this -> exts);
			}
			$this -> config['exts'] = array_map('strtolower', $this -> exts);
		}
	}

	/**
	 * 使用 $this->name 获取配置
	 * @param  string $name 配置名称
	 * @return multitype    配置值
	 */
	public function __get($name) {
		return $this -> config[$name];
	}

	public function __set($name, $value) {
		if (isset($this -> config[$name])) {
			$this -> config[$name] = $value;
			if ($name == 'driverConfig') {
				//改变驱动配置后重置上传驱动
				//注意：必须选改变驱动然后再改变驱动配置
				$this -> setDriver();
			}
		}
	}

	public function __isset($name) {
		return isset($this -> config[$name]);
	}

	/**
	 * 获取最后一次上传错误信息
	 * @return string 错误信息
	 */
	public function getError() {
		return $this -> error;
	}

	/**
	 * 上传单个文件
	 * @param  array  $file 文件数组
	 * @return array        上传成功后的文件信息
	 */
	public function uploadOne($file) {
		$info = $this -> upload(array($file));
		return $info ? $info[0] : $info;
	}

	/**
	 * 上传文件
	 * @param 文件信息数组 $files ，通常是 $_FILES数组
	 */
	public function upload($files = '') {
		if ('' === $files) {
			$files = $_FILES;
		}
		if (empty($files)) {
			$this -> error = '没有上传的文件！';
			return false;
		}

		/* 检测上传根目录 */
		if (!$this -> uploader -> checkRootPath($this -> rootPath)) {
			$this -> error = $this -> uploader -> getError();
			return false;
		}

		/* 检查上传目录 */
		if (!$this -> uploader -> checkSavePath($this -> savePath)) {
			$this -> error = $this -> uploader -> getError();
			return false;
		}

		/* 逐个检测并上传文件 */
		$info = array();
		if (function_exists('finfo_open')) {
			$finfo = finfo_open(FILEINFO_MIME_TYPE);
		}
		// 对上传文件数组信息处理
		$files = $this -> dealFiles($files);

		foreach ($files as $key => $file) {
			$file['name'] = strip_tags($file['name']);
			if (!isset($file['key']))
				$file['key'] = $key;
			/* 通过扩展获取文件类型，可解决FLASH上传$FILES数组返回文件类型错误的问题 */
			if (isset($finfo)) {
				$file['type'] = finfo_file($finfo, $file['tmp_name']);
			}

			/* 获取上传文件后缀，允许上传无后缀文件 */
			$file['ext'] = pathinfo($file['name'], PATHINFO_EXTENSION);

			/* 文件上传检测 */
			if (!$this -> check($file)) {
				continue;
			}

			/* 获取文件hash */
			if ($this -> hash) {
				$file['md5'] = md5_file($file['tmp_name']);
				$file['sha1'] = sha1_file($file['tmp_name']);
			}

			/* 调用回调函数检测文件是否存在 */
			$data = call_user_func($this -> callback, $file);
			if ($this -> callback && $data) {
				if (file_exists('.' . $data['path'])) {
					$info[$key] = $data;
					continue;
				} elseif ($this -> removeTrash) {
					call_user_func($this -> removeTrash, $data);
					//删除垃圾据
				}
			}

			/* 生成保存文件名 */
			$savename = $this -> getSaveName($file);
			if (false == $savename) {
				continue;
			} else {
				$file['savename'] = $savename;
			}

			/* 检测并创建子目录 */
			$subpath = $this -> getSubPath($file['name']);
			if (false === $subpath) {
				continue;
			} else {
				$file['savepath'] = $this -> savePath . $subpath;
			}

			/* 对图像文件进行严格检测 */
			$ext = strtolower($file['ext']);
			if (in_array($ext, array('gif', 'jpg', 'jpeg', 'bmp', 'png', 'swf'))) {
				$imginfo = getimagesize($file['tmp_name']);
				if (empty($imginfo) || ($ext == 'gif' && empty($imginfo['bits']))) {
					$this -> error = '非法图像文件！';
					continue;
				}
			}

			/* 保存文件 并记录保存成功的文件 */
			if ($this -> uploader -> save($file, $this -> replace)) {
				unset($file['error'], $file['tmp_name']);
				$info[$key] = $file;
			} else {
				$this -> error = $this -> uploader -> getError();
			}
		}
		if (isset($finfo)) {
			finfo_close($finfo);
		}
		return empty($info) ? false : $info;
	}

	/**
	 * 转换上传文件数组变量为正确的方式
	 * @access private
	 * @param array $files  上传的文件变量
	 * @return array
	 */
	private function dealFiles($files) {
		$fileArray = array();
		$n = 0;
		foreach ($files as $key => $file) {
			if (is_array($file['name'])) {
				$keys = array_keys($file);
				$count = count($file['name']);
				for ($i = 0; $i < $count; $i++) {
					$fileArray[$n]['key'] = $key;
					foreach ($keys as $_key) {
						$fileArray[$n][$_key] = $file[$_key][$i];
					}
					$n++;
				}
			} else {
				$fileArray = $files;
				break;
			}
		}
		return $fileArray;
	}

	/**
	 * 设置上传驱动
	 * @param string $driver 驱动名称
	 * @param array $config 驱动配置
	 */
	private function setDriver($driver = null, $config = null) {
		$driver = $driver ? : ($this -> driver ? : C('FILE_UPLOAD_TYPE'));
		$config = $config ? : ($this -> driverConfig ? : C('UPLOAD_TYPE_CONFIG'));
		$class = strpos($driver, '\\') ? $driver : 'Think\\Upload\\Driver\\' . ucfirst(strtolower($driver));
		$this -> uploader = new $class($config);
		if (!$this -> uploader) {
			E("不存在上传驱动：{$name}");
		}
	}

	/**
	 * 检查上传的文件
	 * @param array $file 文件信息
	 */
	private function check($file) {
		/* 文件上传失败，捕获错误代码 */

		if ($file['error']) {
			$this -> error($file['error']);
			return false;
		}

		/* 无效上传 */
		if (empty($file['name'])) {
			$this -> error = '未知上传错误！';
		}

		if (!$file['is_move']) {
			/* 检查是否合法上传 */
			if (!is_uploaded_file($file['tmp_name'])) {
				$this -> error = '非法上传文件！';
				return false;
			}
		}

		/* 检查文件大小 */
		if (!$this -> checkSize($file['size'])) {
			$this -> error = '上传文件大小不符！';
			return false;
		}

		/* 检查文件Mime类型 */
		//TODO:FLASH上传的文件获取到的mime类型都为application/octet-stream
		if (!$this -> checkMime($file['type'])) {
			$this -> error = '上传文件MIME类型不允许！';
			return false;
		}

		/* 检查文件后缀 */
		if (!$this -> checkExt($file['ext'])) {
			$this -> error = '上传文件后缀不允许';
			return false;
		}

		/* 通过检测 */
		return true;
	}

	/**
	 * 获取错误代码信息
	 * @param string $errorNo  错误号
	 */
	private function error($errorNo) {
		switch ($errorNo) {
			case 1 :
				$this -> error = '上传的文件超过了 php.ini 中 upload_max_filesize 选项限制的值！';
				break;
			case 2 :
				$this -> error = '上传文件的大小超过了 HTML 表单中 MAX_FILE_SIZE 选项指定的值！';
				break;
			case 3 :
				$this -> error = '文件只有部分被上传！';
				break;
			case 4 :
				$this -> error = '没有文件被上传！';
				break;
			case 6 :
				$this -> error = '找不到临时文件夹！';
				break;
			case 7 :
				$this -> error = '文件写入失败！';
				break;
			default :
				$this -> error = '未知上传错误！';
		}
	}

	/**
	 * 检查文件大小是否合法
	 * @param integer $size 数据
	 */
	private function checkSize($size) {
		return !($size > $this -> maxSize) || (0 == $this -> maxSize);
	}

	/**
	 * 检查上传的文件MIME类型是否合法
	 * @param string $mime 数据
	 */
	private function checkMime($mime) {
		return empty($this -> config['mimes']) ? true : in_array(strtolower($mime), $this -> mimes);
	}

	/**
	 * 检查上传的文件后缀是否合法
	 * @param string $ext 后缀
	 */
	private function checkExt($ext) {
		return empty($this -> config['exts']) ? true : in_array(strtolower($ext), $this -> exts);
	}

	/**
	 * 根据上传文件命名规则取得保存文件名
	 * @param string $file 文件信息
	 */
	private function getSaveName($file) {
		$rule = $this -> saveName;
		if (empty($rule)) {//保持文件名不变
			/* 解决pathinfo中文文件名BUG */
			$filename = substr(pathinfo("_{$file['name']}", PATHINFO_FILENAME), 1);
			$savename = $filename;
		} else {
			$savename = $this -> getName($rule, $file['name']);
			if (empty($savename)) {
				$this -> error = '文件命名规则错误！';
				return false;
			}
		}

		/* 文件保存后缀，支持强制更改文件后缀 */
		$ext = empty($this -> config['saveExt']) ? $file['ext'] : $this -> saveExt;

		return $savename . '.' . $ext;
	}

	/**
	 * 获取子目录的名称
	 * @param array $file  上传的文件信息
	 */
	private function getSubPath($filename) {
		$subpath = '';
		$rule = $this -> subName;
		if ($this -> autoSub && !empty($rule)) {
			$subpath = $this -> getName($rule, $filename) . '/';

			if (!empty($subpath) && !$this -> uploader -> mkdir($this -> savePath . $subpath)) {
				$this -> error = $this -> uploader -> getError();
				return false;
			}
		}
		return $subpath;
	}

	/**
	 * 根据指定的规则获取文件或目录名称
	 * @param  array  $rule     规则
	 * @param  string $filename 原文件名
	 * @return string           文件或目录名称
	 */
	private function getName($rule, $filename) {
		$name = '';
		if (is_array($rule)) {//数组规则
			$func = $rule[0];
			$param = (array)$rule[1];
			foreach ($param as &$value) {
				$value = str_replace('__FILE__', $filename, $value);
			}
			$name = call_user_func_array($func, $param);
		} elseif (is_string($rule)) {//字符串规则
			if (function_exists($rule)) {
				$name = call_user_func($rule);
			} else {
				$name = $rule;
			}
		}
		return $name;
	}

}
